<!DOCTYPE html>


<html lang="zh-CN">


<head>
  <meta charset="utf-8" />
    
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
  <title>
    4-Cookie和Session.md |  
  </title>
  <meta name="generator" content="hexo-theme-ayer">
  
  <link rel="shortcut icon" href="/favicon.ico" />
  
  
<link rel="stylesheet" href="/dist/main.css">

  
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/Shen-Yu/cdn/css/remixicon.min.css">

  
<link rel="stylesheet" href="/css/custom.css">

  
  
<script src="https://cdn.jsdelivr.net/npm/pace-js@1.0.2/pace.min.js"></script>

  
  

  

</head>

</html>

<body>
  <div id="app">
    
      
    <main class="content on">
      <section class="outer">
  <article
  id="post-django/4-Cookie和Session"
  class="article article-type-post"
  itemscope
  itemprop="blogPost"
  data-scroll-reveal
>
  <div class="article-inner">
    
    <header class="article-header">
       
<h1 class="article-title sea-center" style="border-left:0" itemprop="name">
  4-Cookie和Session.md
</h1>
 

    </header>
     
    <div class="article-meta">
      <a href="/2020/11/11/django/4-Cookie%E5%92%8CSession/" class="article-date">
  <time datetime="2020-11-10T16:00:00.000Z" itemprop="datePublished">2020-11-11</time>
</a> 
  <div class="article-category">
    <a class="article-category-link" href="/categories/django/">django</a>
  </div>
  
<div class="word_count">
    <span class="post-time">
        <span class="post-meta-item-icon">
            <i class="ri-quill-pen-line"></i>
            <span class="post-meta-item-text"> 字数统计:</span>
            <span class="post-count">6.2k</span>
        </span>
    </span>

    <span class="post-time">
        &nbsp; | &nbsp;
        <span class="post-meta-item-icon">
            <i class="ri-book-open-line"></i>
            <span class="post-meta-item-text"> 阅读时长≈</span>
            <span class="post-count">26 分钟</span>
        </span>
    </span>
</div>
 
    </div>
      
    <div class="tocbot"></div>




  
    <div class="article-entry" itemprop="articleBody">
       
  <h2 id="Cookie和Session"><a href="#Cookie和Session" class="headerlink" title="Cookie和Session"></a>Cookie和Session</h2><p>我们继续来完成上一章节中的项目，实现“用户登录”的功能，并限制只有登录的用户才能投票。</p>
<h3 id="用户登录的准备工作"><a href="#用户登录的准备工作" class="headerlink" title="用户登录的准备工作"></a>用户登录的准备工作</h3><p>我们先为实现用户登录做一些准备工作。</p>
<ol>
<li><p>创建用户模型。之前我们讲解过如果通过Django的ORM实现从二维表到模型的转换（反向工程），这次我们尝试把模型变成二维表（正向工程）。</p>
 <figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">User</span>(<span class="params">models.Model</span>):</span></span><br><span class="line">    <span class="string">&quot;&quot;&quot;用户&quot;&quot;&quot;</span></span><br><span class="line">    no = models.AutoField(primary_key=<span class="literal">True</span>, verbose_name=<span class="string">&#x27;编号&#x27;</span>)</span><br><span class="line">    username = models.CharField(max_length=<span class="number">20</span>, unique=<span class="literal">True</span>, verbose_name=<span class="string">&#x27;用户名&#x27;</span>)</span><br><span class="line">    password = models.CharField(max_length=<span class="number">32</span>, verbose_name=<span class="string">&#x27;密码&#x27;</span>)</span><br><span class="line">    tel = models.CharField(max_length=<span class="number">20</span>, verbose_name=<span class="string">&#x27;手机号&#x27;</span>)</span><br><span class="line">    reg_date = models.DateTimeField(auto_now_add=<span class="literal">True</span>, verbose_name=<span class="string">&#x27;注册时间&#x27;</span>)</span><br><span class="line">    last_visit = models.DateTimeField(null=<span class="literal">True</span>, verbose_name=<span class="string">&#x27;最后登录时间&#x27;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="class"><span class="keyword">class</span> <span class="title">Meta</span>:</span></span><br><span class="line">        db_table = <span class="string">&#x27;tb_user&#x27;</span></span><br><span class="line">        verbose_name = <span class="string">&#x27;用户&#x27;</span></span><br><span class="line">        verbose_name_plural = <span class="string">&#x27;用户&#x27;</span></span><br></pre></td></tr></table></figure>
</li>
<li><p>使用下面的命令生成迁移文件并执行迁移，将<code>User</code>模型直接变成关系型数据库中的二维表<code>tb_user</code>。</p>
 <figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">python manage.py makemigrations polls</span><br><span class="line">python manage.py migrate polls</span><br></pre></td></tr></table></figure>
</li>
<li><p>用下面的SQL语句直接插入两条测试数据，通常不能讲用户的密码直接保存在数据库中，因此我们将用户密码处理成对应的MD5摘要。MD5消息摘要算法是一种被广泛使用的密码哈希函数（散列函数），可以产生出一个128位（比特）的哈希值（散列值），用于确保信息传输完整一致。在使用哈希值时，通常会将哈希值表示为16进制字符串，因此128位的MD5摘要通常表示为32个十六进制符号。</p>
 <figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">insert</span> <span class="keyword">into</span> <span class="string">`tb_user`</span></span><br><span class="line">    (<span class="string">`username`</span>, <span class="string">`password`</span>, <span class="string">`tel`</span>, <span class="string">`reg_date`</span>)</span><br><span class="line"><span class="keyword">values</span></span><br><span class="line">    (<span class="string">&#x27;wangdachui&#x27;</span>, <span class="string">&#x27;1c63129ae9db9c60c3e8aa94d3e00495&#x27;</span>, <span class="string">&#x27;13122334455&#x27;</span>, <span class="keyword">now</span>()),</span><br><span class="line">    (<span class="string">&#x27;hellokitty&#x27;</span>, <span class="string">&#x27;c6f8cf68e5f68b0aa4680e089ee4742c&#x27;</span>, <span class="string">&#x27;13890006789&#x27;</span>, <span class="keyword">now</span>());</span><br></pre></td></tr></table></figure>

<blockquote>
<p><strong>说明</strong>：上面创建的两个用户<code>wangdachui</code>和<code>hellokitty</code>密码分别是<code>1qaz2wsx</code>和<code>Abc123!!</code>。</p>
</blockquote>
</li>
<li><p>我们在应用下增加一个名为<code>utils.py</code>的模块用来保存需要使用的工具函数。Python标准库中的<code>hashlib</code>模块封装了常用的哈希算法，包括：MD5、SHA1、SHA256等。下面是使用<code>hashlib</code>中的<code>md5</code>类将字符串处理成MD5摘要的函数如下所示。</p>
 <figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> hashlib</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">gen_md5_digest</span>(<span class="params">content</span>):</span></span><br><span class="line">    <span class="keyword">return</span> hashlib.md5(content.encode()).hexdigest()</span><br></pre></td></tr></table></figure>
</li>
<li><p>编写用户登录的视图函数和模板页。</p>
<p> 添加渲染登录页面的视图函数：</p>
 <figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">login</span>(<span class="params">request: HttpRequest</span>) -&gt; HttpResponse:</span></span><br><span class="line">    hint = <span class="string">&#x27;&#x27;</span></span><br><span class="line">    <span class="keyword">return</span> render(request, <span class="string">&#x27;login.html&#x27;</span>, &#123;<span class="string">&#x27;hint&#x27;</span>: hint&#125;)</span><br></pre></td></tr></table></figure>

<p> 增加<code>login.html</code>模板页：</p>
 <figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;!DOCTYPE <span class="meta-keyword">html</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">html</span> <span class="attr">lang</span>=<span class="string">&quot;en&quot;</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">head</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">meta</span> <span class="attr">charset</span>=<span class="string">&quot;UTF-8&quot;</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">title</span>&gt;</span>用户登录<span class="tag">&lt;/<span class="name">title</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">style</span>&gt;</span></span><br><span class="line"><span class="css">        <span class="selector-id">#container</span> &#123;</span></span><br><span class="line">            width: 520px;</span><br><span class="line">            margin: 10px auto;</span><br><span class="line">        &#125;</span><br><span class="line"><span class="css">        <span class="selector-class">.input</span> &#123;</span></span><br><span class="line">            margin: 20px 0;</span><br><span class="line">            width: 460px;</span><br><span class="line">            height: 40px;</span><br><span class="line">        &#125;</span><br><span class="line"><span class="css">        <span class="selector-class">.input</span>&gt;<span class="selector-tag">label</span> &#123;</span></span><br><span class="line">            display: inline-block;</span><br><span class="line">            width: 140px;</span><br><span class="line">            text-align: right;</span><br><span class="line">        &#125;</span><br><span class="line"><span class="css">        <span class="selector-class">.input</span>&gt;<span class="selector-tag">img</span> &#123;</span></span><br><span class="line">            width: 150px;</span><br><span class="line">            vertical-align: middle;</span><br><span class="line">        &#125;</span><br><span class="line"><span class="css">        <span class="selector-tag">input</span><span class="selector-attr">[name=captcha]</span> &#123;</span></span><br><span class="line">            vertical-align: middle;</span><br><span class="line">        &#125;</span><br><span class="line">        form+div &#123;</span><br><span class="line">            margin-top: 20px;</span><br><span class="line">        &#125;</span><br><span class="line">        form+div&gt;a &#123;</span><br><span class="line">            text-decoration: none;</span><br><span class="line">            color: darkcyan;</span><br><span class="line"><span class="css">            <span class="selector-tag">font-size</span>: 1<span class="selector-class">.2em</span>;</span></span><br><span class="line">        &#125;</span><br><span class="line"><span class="css">        <span class="selector-class">.button</span> &#123;</span></span><br><span class="line">            width: 500px;</span><br><span class="line">            text-align: center;</span><br><span class="line">            margin-top: 20px;</span><br><span class="line">        &#125;</span><br><span class="line"><span class="css">        <span class="selector-class">.hint</span> &#123;</span></span><br><span class="line">            color: red;</span><br><span class="line">            font-size: 12px;</span><br><span class="line">        &#125;</span><br><span class="line">    <span class="tag">&lt;/<span class="name">style</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">head</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">body</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">div</span> <span class="attr">id</span>=<span class="string">&quot;container&quot;</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">h1</span>&gt;</span>用户登录<span class="tag">&lt;/<span class="name">h1</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">hr</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">p</span> <span class="attr">class</span>=<span class="string">&quot;hint&quot;</span>&gt;</span>&#123;&#123; hint &#125;&#125;<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">form</span> <span class="attr">action</span>=<span class="string">&quot;/login/&quot;</span> <span class="attr">method</span>=<span class="string">&quot;post&quot;</span>&gt;</span></span><br><span class="line">            &#123;% csrf_token %&#125;</span><br><span class="line">            <span class="tag">&lt;<span class="name">fieldset</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;<span class="name">legend</span>&gt;</span>用户信息<span class="tag">&lt;/<span class="name">legend</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;input&quot;</span>&gt;</span></span><br><span class="line">                    <span class="tag">&lt;<span class="name">label</span>&gt;</span>用户名：<span class="tag">&lt;/<span class="name">label</span>&gt;</span></span><br><span class="line">                    <span class="tag">&lt;<span class="name">input</span> <span class="attr">type</span>=<span class="string">&quot;text&quot;</span> <span class="attr">name</span>=<span class="string">&quot;username&quot;</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;input&quot;</span>&gt;</span></span><br><span class="line">                    <span class="tag">&lt;<span class="name">label</span>&gt;</span>密码：<span class="tag">&lt;/<span class="name">label</span>&gt;</span></span><br><span class="line">                    <span class="tag">&lt;<span class="name">input</span> <span class="attr">type</span>=<span class="string">&quot;password&quot;</span> <span class="attr">name</span>=<span class="string">&quot;password&quot;</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;input&quot;</span>&gt;</span></span><br><span class="line">                    <span class="tag">&lt;<span class="name">label</span>&gt;</span>验证码：<span class="tag">&lt;/<span class="name">label</span>&gt;</span></span><br><span class="line">                    <span class="tag">&lt;<span class="name">input</span> <span class="attr">type</span>=<span class="string">&quot;text&quot;</span> <span class="attr">name</span>=<span class="string">&quot;captcha&quot;</span>&gt;</span></span><br><span class="line">                    <span class="tag">&lt;<span class="name">img</span> <span class="attr">id</span>=<span class="string">&quot;code&quot;</span> <span class="attr">src</span>=<span class="string">&quot;/captcha/&quot;</span> <span class="attr">alt</span>=<span class="string">&quot;&quot;</span> <span class="attr">width</span>=<span class="string">&quot;150&quot;</span> <span class="attr">height</span>=<span class="string">&quot;40&quot;</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;/<span class="name">fieldset</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;button&quot;</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;<span class="name">input</span> <span class="attr">type</span>=<span class="string">&quot;submit&quot;</span> <span class="attr">value</span>=<span class="string">&quot;登录&quot;</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;<span class="name">input</span> <span class="attr">type</span>=<span class="string">&quot;reset&quot;</span> <span class="attr">value</span>=<span class="string">&quot;重置&quot;</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">form</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">div</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">a</span> <span class="attr">href</span>=<span class="string">&quot;/&quot;</span>&gt;</span>返回首页<span class="tag">&lt;/<span class="name">a</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">a</span> <span class="attr">href</span>=<span class="string">&quot;/register/&quot;</span>&gt;</span>注册新用户<span class="tag">&lt;/<span class="name">a</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">body</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">html</span>&gt;</span></span><br></pre></td></tr></table></figure>

<p> 注意，在上面的表单中，我们使用了模板指令<code>&#123;% csrf_token %&#125;</code>为表单添加一个隐藏域（大家可以在浏览器中显示网页源代码就可以看到这个指令生成的<code>type</code>属性为<code>hidden</code>的<code>input</code>标签），它的作用是在表单中生成一个随机令牌（token）来防范<a target="_blank" rel="noopener" href="https://zh.wikipedia.org/wiki/%E8%B7%A8%E7%AB%99%E8%AF%B7%E6%B1%82%E4%BC%AA%E9%80%A0">跨站请求伪造</a>（简称为CSRF），这也是Django在提交表单时的硬性要求。如果我们的表单中没有这样的令牌，那么提交表单时，Django框架会产生一个响应状态码为<code>403</code>的响应（禁止访问），除非我们设置了免除CSRF令牌。下图是一个关于CSRF简单生动的例子。</p>
<p> <img src="http://iubest.gitee.io/pic/csrf-simple.png" alt="img"></p>
</li>
</ol>
<p>接下来，我们可以编写提供验证码和实现用户登录的视图函数，在此之前，我们先说说一个Web应用实现用户跟踪的方式以及Django框架对实现用户跟踪所提供的支持。对一个Web应用来说，用户登录成功后必然要让服务器能够记住该用户已经登录，这样服务器才能为这个用户提供更好的服务，而且上面说到的CSRF也是通过钓鱼网站来套取用户登录信息进行恶意操作的攻击手段，这些都是以用户跟踪技术为基础的。在理解了这些背景知识后，我们就清楚用户登录时到底需要执行哪些操作。</p>
<h3 id="实现用户跟踪"><a href="#实现用户跟踪" class="headerlink" title="实现用户跟踪"></a>实现用户跟踪</h3><p>如今，一个网站如果不通过某种方式记住你是谁以及你之前在网站的活动情况，失去的就是网站的可用性和便利性，继而很有可能导致网站用户的流式，所以记住一个用户（更专业的说法叫<strong>用户跟踪</strong>）对绝大多数Web应用来说都是必需的功能。</p>
<p>在服务器端，我们想记住一个用户最简单的办法就是创建一个对象，通过这个对象就可以把用户相关的信息都保存起来，这个对象就是我们常说的session（用户会话对象）。那么问题来了，HTTP本身是一个<strong>无连接</strong>（每次请求和响应的过程中，服务器一旦完成对客户端请求的响应之后就断开连接）、<strong>无状态</strong>（客户端再次发起对服务器的请求时，服务器无法得知这个客户端之前的任何信息）的协议，即便服务器通过session对象保留了用户数据，还得通过某种方式来确定当前的请求与之前保存过的哪一个session是有关联的。相信很多人都能想到，我们可以给每个session对象分配一个全局唯一的标识符来识别session对象，我们姑且称之为sessionid，每次客户端发起请求时，只要携带上这个sessionid，就有办法找到与之对应的session对象，从而实现在两次请求之间记住该用户的信息，也就是我们之前说的用户跟踪。</p>
<p>要让客户端记住并在每次请求时带上sessionid又有以下几种做法：</p>
<ol>
<li><p>URL重写。所谓URL重写就是在URL中携带sessionid，例如：<code>http://www.example.com/index.html?sessionid=123456</code>，服务器通过获取sessionid参数的值来取到与之对应的session对象。</p>
</li>
<li><p>隐藏域（隐式表单域）。在提交表单的时候，可以通过在表单中设置隐藏域向服务器发送额外的数据。例如：<code>&lt;input type=&quot;hidden&quot; name=&quot;sessionid&quot; value=&quot;123456&quot;&gt;</code>。</p>
</li>
<li><p>本地存储。现在的浏览器都支持多种本地存储方案，包括：cookie、localStorage、sessionStorage、IndexedDB等。在这些方案中，cookie是历史最为悠久也是被诟病得最多的一种方案，也是我们接下来首先为大家讲解的一种方案。简单的说，cookie是一种以键值对方式保存在浏览器临时文件中的数据，每次请求时，请求头中会携带本站点的cookie到服务器，那么只要将sessionid写入cookie，下次请求时服务器只要读取请求头中的cookie就能够获得这个sessionid，如下图所示。</p>
<p><img src="http://iubest.gitee.io/pic/sessionid_from_cookie.png" alt="img"></p>
<p>在HTML5时代要，除了cookie，还可以使用新的本地存储API来保存数据，就是刚才提到的localStorage、sessionStorage、IndexedDB等技术，如下图所示。</p>
<p><img src="http://iubest.gitee.io/pic/cookie_xstorage_indexeddb.png" alt="img"></p>
</li>
</ol>
<p><strong>总结一下</strong>，要实现用户跟踪，服务器端可以为每个用户会话创建一个session对象并将session对象的ID写入到浏览器的cookie中；用户下次请求服务器时，浏览器会在HTTP请求头中携带该网站保存的cookie信息，这样服务器就可以从cookie中找到session对象的ID并根据此ID获取到之前创建的session对象；由于session对象可以用键值对的方式保存用户数据，这样之前保存在session对象中的信息可以悉数取出，服务器也可以根据这些信息判定用户身份和了解用户偏好，为用户提供更好的个性化服务。</p>
<h3 id="Django框架对session的支持"><a href="#Django框架对session的支持" class="headerlink" title="Django框架对session的支持"></a>Django框架对session的支持</h3><p>在创建Django项目时，默认的配置文件<code>settings.py</code>文件中已经激活了一个名为<code>SessionMiddleware</code>的中间件（关于中间件的知识我们在后面的章节做详细讲解，这里只需要知道它的存在即可），因为这个中间件的存在，我们可以直接通过请求对象的<code>session</code>属性来操作会话对象。前面我们说过，<code>session</code>属性是一个像字典一样可以读写数据的容器对象，因此我们可以使用“键值对”的方式来保留用户数据。与此同时，<code>SessionMiddleware</code>中间件还封装了对cookie的操作，在cookie中保存了sessionid，这一点我们在上面已经提到过了。</p>
<p>在默认情况下，Django将session的数据序列化后保存在关系型数据库中，在Django 1.6以后的版本中，默认的序列化数据的方式是JSON序列化，而在此之前一直使用Pickle序列化。JSON序列化和Pickle序列化的差别在于前者将对象序列化为字符串（字符形式），而后者将对象序列化为字节串（二进制形式），因为安全方面的原因，JSON序列化成为了目前Django框架默认序列化数据的方式，这就要求在我们保存在session中的数据必须是能够JSON序列化的，否则就会引发异常。还有一点需要说明的是，使用关系型数据库保存session中的数据在大多数时候并不是最好的选择，因为数据库可能会承受巨大的压力而成为系统性能的瓶颈，在后面的章节中我们会告诉大家如何将session保存到缓存服务中以提升系统的性能。</p>
<h3 id="实现用户登录验证"><a href="#实现用户登录验证" class="headerlink" title="实现用户登录验证"></a>实现用户登录验证</h3><p>首先，我们在刚才的<code>polls/utils.py</code>文件中编写生成随机验证码的函数<code>gen_random_code</code>，内容如下所示。</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> random</span><br><span class="line"></span><br><span class="line">ALL_CHARS = <span class="string">&#x27;0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ&#x27;</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">gen_random_code</span>(<span class="params">length=<span class="number">4</span></span>):</span></span><br><span class="line">    <span class="keyword">return</span> <span class="string">&#x27;&#x27;</span>.join(random.choices(ALL_CHARS, k=length))</span><br></pre></td></tr></table></figure>

<p>编写生成验证码图片的类<code>Captcha</code>。</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br><span class="line">157</span><br><span class="line">158</span><br><span class="line">159</span><br><span class="line">160</span><br><span class="line">161</span><br><span class="line">162</span><br><span class="line">163</span><br><span class="line">164</span><br><span class="line">165</span><br><span class="line">166</span><br><span class="line">167</span><br><span class="line">168</span><br><span class="line">169</span><br><span class="line">170</span><br><span class="line">171</span><br><span class="line">172</span><br><span class="line">173</span><br><span class="line">174</span><br><span class="line">175</span><br><span class="line">176</span><br><span class="line">177</span><br><span class="line">178</span><br><span class="line">179</span><br><span class="line">180</span><br><span class="line">181</span><br><span class="line">182</span><br><span class="line">183</span><br><span class="line">184</span><br><span class="line">185</span><br><span class="line">186</span><br><span class="line">187</span><br><span class="line">188</span><br><span class="line">189</span><br><span class="line">190</span><br><span class="line">191</span><br><span class="line">192</span><br><span class="line">193</span><br><span class="line">194</span><br><span class="line">195</span><br><span class="line">196</span><br><span class="line">197</span><br><span class="line">198</span><br><span class="line">199</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">图片验证码</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="keyword">import</span> os</span><br><span class="line"><span class="keyword">import</span> random</span><br><span class="line"><span class="keyword">from</span> io <span class="keyword">import</span> BytesIO</span><br><span class="line"></span><br><span class="line"><span class="keyword">from</span> PIL <span class="keyword">import</span> Image</span><br><span class="line"><span class="keyword">from</span> PIL <span class="keyword">import</span> ImageFilter</span><br><span class="line"><span class="keyword">from</span> PIL.ImageDraw <span class="keyword">import</span> Draw</span><br><span class="line"><span class="keyword">from</span> PIL.ImageFont <span class="keyword">import</span> truetype</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Bezier</span>:</span></span><br><span class="line">    <span class="string">&quot;&quot;&quot;贝塞尔曲线&quot;&quot;&quot;</span></span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">__init__</span>(<span class="params">self</span>):</span></span><br><span class="line">        self.tsequence = tuple([t / <span class="number">20.0</span> <span class="keyword">for</span> t <span class="keyword">in</span> range(<span class="number">21</span>)])</span><br><span class="line">        self.beziers = &#123;&#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">make_bezier</span>(<span class="params">self, n</span>):</span></span><br><span class="line">        <span class="string">&quot;&quot;&quot;绘制贝塞尔曲线&quot;&quot;&quot;</span></span><br><span class="line">        <span class="keyword">try</span>:</span><br><span class="line">            <span class="keyword">return</span> self.beziers[n]</span><br><span class="line">        <span class="keyword">except</span> KeyError:</span><br><span class="line">            combinations = pascal_row(n - <span class="number">1</span>)</span><br><span class="line">            result = []</span><br><span class="line">            <span class="keyword">for</span> t <span class="keyword">in</span> self.tsequence:</span><br><span class="line">                tpowers = (t ** i <span class="keyword">for</span> i <span class="keyword">in</span> range(n))</span><br><span class="line">                upowers = ((<span class="number">1</span> - t) ** i <span class="keyword">for</span> i <span class="keyword">in</span> range(n - <span class="number">1</span>, <span class="number">-1</span>, <span class="number">-1</span>))</span><br><span class="line">                coefs = [c * a * b <span class="keyword">for</span> c, a, b <span class="keyword">in</span> zip(combinations,</span><br><span class="line">                                                      tpowers, upowers)]</span><br><span class="line">                result.append(coefs)</span><br><span class="line">            self.beziers[n] = result</span><br><span class="line">            <span class="keyword">return</span> result</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Captcha</span>:</span></span><br><span class="line">    <span class="string">&quot;&quot;&quot;验证码&quot;&quot;&quot;</span></span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">__init__</span>(<span class="params">self, width, height, fonts=None, color=None</span>):</span></span><br><span class="line">        self._image = <span class="literal">None</span></span><br><span class="line">        self._fonts = fonts <span class="keyword">if</span> fonts <span class="keyword">else</span> \</span><br><span class="line">            [os.path.join(os.path.dirname(__file__), <span class="string">&#x27;fonts&#x27;</span>, font)</span><br><span class="line">             <span class="keyword">for</span> font <span class="keyword">in</span> [<span class="string">&#x27;Arial.ttf&#x27;</span>, <span class="string">&#x27;Georgia.ttf&#x27;</span>, <span class="string">&#x27;Action.ttf&#x27;</span>]]</span><br><span class="line">        self._color = color <span class="keyword">if</span> color <span class="keyword">else</span> random_color(<span class="number">0</span>, <span class="number">200</span>, random.randint(<span class="number">220</span>, <span class="number">255</span>))</span><br><span class="line">        self._width, self._height = width, height</span><br><span class="line"></span><br><span class="line"><span class="meta">    @classmethod</span></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">instance</span>(<span class="params">cls, width=<span class="number">200</span>, height=<span class="number">75</span></span>):</span></span><br><span class="line">        <span class="string">&quot;&quot;&quot;用于获取Captcha对象的类方法&quot;&quot;&quot;</span></span><br><span class="line">        prop_name = <span class="string">f&#x27;_instance_<span class="subst">&#123;width&#125;</span>_<span class="subst">&#123;height&#125;</span>&#x27;</span></span><br><span class="line">        <span class="keyword">if</span> <span class="keyword">not</span> hasattr(cls, prop_name):</span><br><span class="line">            setattr(cls, prop_name, cls(width, height))</span><br><span class="line">        <span class="keyword">return</span> getattr(cls, prop_name)</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">_background</span>(<span class="params">self</span>):</span></span><br><span class="line">        <span class="string">&quot;&quot;&quot;绘制背景&quot;&quot;&quot;</span></span><br><span class="line">        Draw(self._image).rectangle([(<span class="number">0</span>, <span class="number">0</span>), self._image.size],</span><br><span class="line">                                    fill=random_color(<span class="number">230</span>, <span class="number">255</span>))</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">_smooth</span>(<span class="params">self</span>):</span></span><br><span class="line">        <span class="string">&quot;&quot;&quot;平滑图像&quot;&quot;&quot;</span></span><br><span class="line">        <span class="keyword">return</span> self._image.filter(ImageFilter.SMOOTH)</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">_curve</span>(<span class="params">self, width=<span class="number">4</span>, number=<span class="number">6</span>, color=None</span>):</span></span><br><span class="line">        <span class="string">&quot;&quot;&quot;绘制曲线&quot;&quot;&quot;</span></span><br><span class="line">        dx, height = self._image.size</span><br><span class="line">        dx /= number</span><br><span class="line">        path = [(dx * i, random.randint(<span class="number">0</span>, height))</span><br><span class="line">                <span class="keyword">for</span> i <span class="keyword">in</span> range(<span class="number">1</span>, number)]</span><br><span class="line">        bcoefs = Bezier().make_bezier(number - <span class="number">1</span>)</span><br><span class="line">        points = []</span><br><span class="line">        <span class="keyword">for</span> coefs <span class="keyword">in</span> bcoefs:</span><br><span class="line">            points.append(tuple(sum([coef * p <span class="keyword">for</span> coef, p <span class="keyword">in</span> zip(coefs, ps)])</span><br><span class="line">                                <span class="keyword">for</span> ps <span class="keyword">in</span> zip(*path)))</span><br><span class="line">        Draw(self._image).line(points, fill=color <span class="keyword">if</span> color <span class="keyword">else</span> self._color, width=width)</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">_noise</span>(<span class="params">self, number=<span class="number">50</span>, level=<span class="number">2</span>, color=None</span>):</span></span><br><span class="line">        <span class="string">&quot;&quot;&quot;绘制扰码&quot;&quot;&quot;</span></span><br><span class="line">        width, height = self._image.size</span><br><span class="line">        dx, dy = width / <span class="number">10</span>, height / <span class="number">10</span></span><br><span class="line">        width, height = width - dx, height - dy</span><br><span class="line">        draw = Draw(self._image)</span><br><span class="line">        <span class="keyword">for</span> i <span class="keyword">in</span> range(number):</span><br><span class="line">            x = int(random.uniform(dx, width))</span><br><span class="line">            y = int(random.uniform(dy, height))</span><br><span class="line">            draw.line(((x, y), (x + level, y)),</span><br><span class="line">                      fill=color <span class="keyword">if</span> color <span class="keyword">else</span> self._color, width=level)</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">_text</span>(<span class="params">self, captcha_text, fonts, font_sizes=None, drawings=None, squeeze_factor=<span class="number">0.75</span>, color=None</span>):</span></span><br><span class="line">        <span class="string">&quot;&quot;&quot;绘制文本&quot;&quot;&quot;</span></span><br><span class="line">        color = color <span class="keyword">if</span> color <span class="keyword">else</span> self._color</span><br><span class="line">        fonts = tuple([truetype(name, size)</span><br><span class="line">                       <span class="keyword">for</span> name <span class="keyword">in</span> fonts</span><br><span class="line">                       <span class="keyword">for</span> size <span class="keyword">in</span> font_sizes <span class="keyword">or</span> (<span class="number">65</span>, <span class="number">70</span>, <span class="number">75</span>)])</span><br><span class="line">        draw = Draw(self._image)</span><br><span class="line">        char_images = []</span><br><span class="line">        <span class="keyword">for</span> c <span class="keyword">in</span> captcha_text:</span><br><span class="line">            font = random.choice(fonts)</span><br><span class="line">            c_width, c_height = draw.textsize(c, font=font)</span><br><span class="line">            char_image = Image.new(<span class="string">&#x27;RGB&#x27;</span>, (c_width, c_height), (<span class="number">0</span>, <span class="number">0</span>, <span class="number">0</span>))</span><br><span class="line">            char_draw = Draw(char_image)</span><br><span class="line">            char_draw.text((<span class="number">0</span>, <span class="number">0</span>), c, font=font, fill=color)</span><br><span class="line">            char_image = char_image.crop(char_image.getbbox())</span><br><span class="line">            <span class="keyword">for</span> drawing <span class="keyword">in</span> drawings:</span><br><span class="line">                d = getattr(self, drawing)</span><br><span class="line">                char_image = d(char_image)</span><br><span class="line">            char_images.append(char_image)</span><br><span class="line">        width, height = self._image.size</span><br><span class="line">        offset = int((width - sum(int(i.size[<span class="number">0</span>] * squeeze_factor)</span><br><span class="line">                                  <span class="keyword">for</span> i <span class="keyword">in</span> char_images[:<span class="number">-1</span>]) -</span><br><span class="line">                      char_images[<span class="number">-1</span>].size[<span class="number">0</span>]) / <span class="number">2</span>)</span><br><span class="line">        <span class="keyword">for</span> char_image <span class="keyword">in</span> char_images:</span><br><span class="line">            c_width, c_height = char_image.size</span><br><span class="line">            mask = char_image.convert(<span class="string">&#x27;L&#x27;</span>).point(<span class="keyword">lambda</span> i: i * <span class="number">1.97</span>)</span><br><span class="line">            self._image.paste(char_image,</span><br><span class="line">                              (offset, int((height - c_height) / <span class="number">2</span>)),</span><br><span class="line">                              mask)</span><br><span class="line">            offset += int(c_width * squeeze_factor)</span><br><span class="line"></span><br><span class="line"><span class="meta">    @staticmethod</span></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">_warp</span>(<span class="params">image, dx_factor=<span class="number">0.3</span>, dy_factor=<span class="number">0.3</span></span>):</span></span><br><span class="line">        <span class="string">&quot;&quot;&quot;图像扭曲&quot;&quot;&quot;</span></span><br><span class="line">        width, height = image.size</span><br><span class="line">        dx = width * dx_factor</span><br><span class="line">        dy = height * dy_factor</span><br><span class="line">        x1 = int(random.uniform(-dx, dx))</span><br><span class="line">        y1 = int(random.uniform(-dy, dy))</span><br><span class="line">        x2 = int(random.uniform(-dx, dx))</span><br><span class="line">        y2 = int(random.uniform(-dy, dy))</span><br><span class="line">        warp_image = Image.new(</span><br><span class="line">            <span class="string">&#x27;RGB&#x27;</span>,</span><br><span class="line">            (width + abs(x1) + abs(x2), height + abs(y1) + abs(y2)))</span><br><span class="line">        warp_image.paste(image, (abs(x1), abs(y1)))</span><br><span class="line">        width2, height2 = warp_image.size</span><br><span class="line">        <span class="keyword">return</span> warp_image.transform(</span><br><span class="line">            (width, height),</span><br><span class="line">            Image.QUAD,</span><br><span class="line">            (x1, y1, -x1, height2 - y2, width2 + x2, height2 + y2, width2 - x2, -y1))</span><br><span class="line"></span><br><span class="line"><span class="meta">    @staticmethod</span></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">_offset</span>(<span class="params">image, dx_factor=<span class="number">0.1</span>, dy_factor=<span class="number">0.2</span></span>):</span></span><br><span class="line">        <span class="string">&quot;&quot;&quot;图像偏移&quot;&quot;&quot;</span></span><br><span class="line">        width, height = image.size</span><br><span class="line">        dx = int(random.random() * width * dx_factor)</span><br><span class="line">        dy = int(random.random() * height * dy_factor)</span><br><span class="line">        offset_image = Image.new(<span class="string">&#x27;RGB&#x27;</span>, (width + dx, height + dy))</span><br><span class="line">        offset_image.paste(image, (dx, dy))</span><br><span class="line">        <span class="keyword">return</span> offset_image</span><br><span class="line"></span><br><span class="line"><span class="meta">    @staticmethod</span></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">_rotate</span>(<span class="params">image, angle=<span class="number">25</span></span>):</span></span><br><span class="line">        <span class="string">&quot;&quot;&quot;图像旋转&quot;&quot;&quot;</span></span><br><span class="line">        <span class="keyword">return</span> image.rotate(random.uniform(-angle, angle),</span><br><span class="line">                            Image.BILINEAR, expand=<span class="number">1</span>)</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">generate</span>(<span class="params">self, captcha_text=<span class="string">&#x27;&#x27;</span>, fmt=<span class="string">&#x27;PNG&#x27;</span></span>):</span></span><br><span class="line">        <span class="string">&quot;&quot;&quot;生成验证码(文字和图片)</span></span><br><span class="line"><span class="string">        :param captcha_text: 验证码文字</span></span><br><span class="line"><span class="string">        :param fmt: 生成的验证码图片格式</span></span><br><span class="line"><span class="string">        :return: 验证码图片的二进制数据</span></span><br><span class="line"><span class="string">        &quot;&quot;&quot;</span></span><br><span class="line">        self._image = Image.new(<span class="string">&#x27;RGB&#x27;</span>, (self._width, self._height), (<span class="number">255</span>, <span class="number">255</span>, <span class="number">255</span>))</span><br><span class="line">        self._background()</span><br><span class="line">        self._text(captcha_text, self._fonts,</span><br><span class="line">                   drawings=[<span class="string">&#x27;_warp&#x27;</span>, <span class="string">&#x27;_rotate&#x27;</span>, <span class="string">&#x27;_offset&#x27;</span>])</span><br><span class="line">        self._curve()</span><br><span class="line">        self._noise()</span><br><span class="line">        self._smooth()</span><br><span class="line">        image_bytes = BytesIO()</span><br><span class="line">        self._image.save(image_bytes, format=fmt)</span><br><span class="line">        <span class="keyword">return</span> image_bytes.getvalue()</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">pascal_row</span>(<span class="params">n=<span class="number">0</span></span>):</span></span><br><span class="line">    <span class="string">&quot;&quot;&quot;生成毕达哥拉斯三角形（杨辉三角）&quot;&quot;&quot;</span></span><br><span class="line">    result = [<span class="number">1</span>]</span><br><span class="line">    x, numerator = <span class="number">1</span>, n</span><br><span class="line">    <span class="keyword">for</span> denominator <span class="keyword">in</span> range(<span class="number">1</span>, n // <span class="number">2</span> + <span class="number">1</span>):</span><br><span class="line">        x *= numerator</span><br><span class="line">        x /= denominator</span><br><span class="line">        result.append(x)</span><br><span class="line">        numerator -= <span class="number">1</span></span><br><span class="line">    <span class="keyword">if</span> n &amp; <span class="number">1</span> == <span class="number">0</span>:</span><br><span class="line">        result.extend(reversed(result[:<span class="number">-1</span>]))</span><br><span class="line">    <span class="keyword">else</span>:</span><br><span class="line">        result.extend(reversed(result))</span><br><span class="line">    <span class="keyword">return</span> result</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">random_color</span>(<span class="params">start=<span class="number">0</span>, end=<span class="number">255</span>, opacity=<span class="number">255</span></span>):</span></span><br><span class="line">    <span class="string">&quot;&quot;&quot;获得随机颜色&quot;&quot;&quot;</span></span><br><span class="line">    red = random.randint(start, end)</span><br><span class="line">    green = random.randint(start, end)</span><br><span class="line">    blue = random.randint(start, end)</span><br><span class="line">    <span class="keyword">if</span> opacity <span class="keyword">is</span> <span class="literal">None</span>:</span><br><span class="line">        <span class="keyword">return</span> red, green, blue</span><br><span class="line">    <span class="keyword">return</span> red, green, blue, opacity</span><br></pre></td></tr></table></figure>

<blockquote>
<p><strong>说明</strong>：上面的代码中用到了三个字体文件，字体文件位于<code>polls/fonts</code>目录下，大家可以自行添加字体文件，但是需要注意字体文件的文件名跟上面代码的第45行保持一致。</p>
</blockquote>
<p>接下来，我们先完成提供验证码的视图函数。</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">get_captcha</span>(<span class="params">request: HttpRequest</span>) -&gt; HttpResponse:</span></span><br><span class="line">    <span class="string">&quot;&quot;&quot;验证码&quot;&quot;&quot;</span></span><br><span class="line">    captcha_text = gen_random_code()</span><br><span class="line">    request.session[<span class="string">&#x27;captcha&#x27;</span>] = captcha_text</span><br><span class="line">    image_data = Captcha.instance().generate(captcha_text)</span><br><span class="line">    <span class="keyword">return</span> HttpResponse(image_data, content_type=<span class="string">&#x27;image/png&#x27;</span>)</span><br></pre></td></tr></table></figure>

<p>注意上面代码中的第4行，我们将随机生成的验证码字符串保存到session中，稍后用户登录时，我们要将保存在session中的验证码字符串和用户输入的验证码字符串进行比对，如果用户输入了正确的验证码才能够执行后续的登录流程，代码如下所示。</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">login</span>(<span class="params">request: HttpRequest</span>) -&gt; HttpResponse:</span></span><br><span class="line">    hint = <span class="string">&#x27;&#x27;</span></span><br><span class="line">    <span class="keyword">if</span> request.method == <span class="string">&#x27;POST&#x27;</span>:</span><br><span class="line">        username = request.POST.get(<span class="string">&#x27;username&#x27;</span>)</span><br><span class="line">        password = request.POST.get(<span class="string">&#x27;password&#x27;</span>)</span><br><span class="line">        <span class="keyword">if</span> username <span class="keyword">and</span> password:</span><br><span class="line">            password = gen_md5_digest(password)</span><br><span class="line">            user = User.objects.filter(username=username, password=password).first()</span><br><span class="line">            <span class="keyword">if</span> user:</span><br><span class="line">                request.session[<span class="string">&#x27;userid&#x27;</span>] = user.no</span><br><span class="line">                request.session[<span class="string">&#x27;username&#x27;</span>] = user.username</span><br><span class="line">                <span class="keyword">return</span> redirect(<span class="string">&#x27;/&#x27;</span>)</span><br><span class="line">            <span class="keyword">else</span>:</span><br><span class="line">                hint = <span class="string">&#x27;用户名或密码错误&#x27;</span></span><br><span class="line">        <span class="keyword">else</span>:</span><br><span class="line">            hint = <span class="string">&#x27;请输入有效的用户名和密码&#x27;</span></span><br><span class="line">    <span class="keyword">return</span> render(request, <span class="string">&#x27;login.html&#x27;</span>, &#123;<span class="string">&#x27;hint&#x27;</span>: hint&#125;)</span><br></pre></td></tr></table></figure>

<blockquote>
<p><strong>说明</strong>：上面的代码没有对用户名和密码没有进行验证，实际项目中建议使用正则表达式验证用户输入信息，否则有可能将无效的数据交给数据库进行处理或者造成其他安全方面的隐患。</p>
</blockquote>
<p>上面的代码中，我们设定了登录成功后会在session中保存用户的编号（<code>userid</code>）和用户名（<code>username</code>），页面会重定向到首页。接下来我们可以稍微对首页的代码进行调整，在页面的右上角显示出登录用户的用户名。我们将这段代码单独写成了一个名为header.html的HTML文件，首页中可以通过在<code>&lt;body&gt;</code>标签中添加<code>&#123;% include 'header.html' %&#125;</code>来包含这个页面，代码如下所示。</p>
<figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;user&quot;</span>&gt;</span></span><br><span class="line">    &#123;% if request.session.userid %&#125;</span><br><span class="line">    <span class="tag">&lt;<span class="name">span</span>&gt;</span>&#123;&#123; request.session.username &#125;&#125;<span class="tag">&lt;/<span class="name">span</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">a</span> <span class="attr">href</span>=<span class="string">&quot;/logout&quot;</span>&gt;</span>注销<span class="tag">&lt;/<span class="name">a</span>&gt;</span></span><br><span class="line">    &#123;% else %&#125;</span><br><span class="line">    <span class="tag">&lt;<span class="name">a</span> <span class="attr">href</span>=<span class="string">&quot;/login&quot;</span>&gt;</span>登录<span class="tag">&lt;/<span class="name">a</span>&gt;</span><span class="symbol">&amp;nbsp;</span><span class="symbol">&amp;nbsp;</span></span><br><span class="line">    &#123;% endif %&#125;</span><br><span class="line">    <span class="tag">&lt;<span class="name">a</span> <span class="attr">href</span>=<span class="string">&quot;/register&quot;</span>&gt;</span>注册<span class="tag">&lt;/<span class="name">a</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br></pre></td></tr></table></figure>

<p>如果用户没有登录，页面会显示登录和注册的超链接；而用户登录成功后，页面上会显示用户名和注销的链接，注销链接对应的视图函数如下所示，URL的映射与之前讲过的类似，不再赘述。</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">logout</span>(<span class="params">request</span>):</span></span><br><span class="line">    <span class="string">&quot;&quot;&quot;注销&quot;&quot;&quot;</span></span><br><span class="line">    request.session.flush()</span><br><span class="line">    <span class="keyword">return</span> redirect(<span class="string">&#x27;/&#x27;</span>)</span><br></pre></td></tr></table></figure>

<p>上面的代码通过session对象<code>flush</code>方法来销毁session，一方面清除了服务器上session对象保存的用户数据，一方面将保存在浏览器cookie中的sessionid删除掉，稍后我们会对如何读写cookie的操作加以说明。</p>
<p>我们可以通过项目使用的数据库中名为<code>django_session</code> 的表来找到所有的session，该表的结构如下所示：</p>
<table>
<thead>
<tr>
<th>session_key</th>
<th>session_data</th>
<th>expire_date</th>
</tr>
</thead>
<tbody><tr>
<td>c9g2gt5cxo0k2evykgpejhic5ae7bfpl</td>
<td>MmI4YzViYjJhOGMyMDJkY2M5Yzg3…</td>
<td>2019-05-25 23:16:13.898522</td>
</tr>
</tbody></table>
<p>其中，第1列就是浏览器cookie中保存的sessionid；第2列是经过BASE64编码后的session中的数据，如果使用Python的<code>base64</code>对其进行解码，解码的过程和结果如下所示。</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> base64</span><br><span class="line"></span><br><span class="line">base64.b64decode(<span class="string">&#x27;MmI4YzViYjJhOGMyMDJkY2M5Yzg3ZWIyZGViZmUzYmYxNzdlNDdmZjp7ImNhcHRjaGEiOiJzS3d0Iiwibm8iOjEsInVzZXJuYW1lIjoiamFja2ZydWVkIn0=&#x27;</span>)</span><br></pre></td></tr></table></figure>

<p>第3列是session的过期时间，session过期后浏览器保存的cookie中的sessionid就会失效，但是数据库中的这条对应的记录仍然会存在，如果想清除过期的数据，可以使用下面的命令。</p>
<figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">python manage.py clearsessions</span><br></pre></td></tr></table></figure>

<p>Django框架默认的session过期时间为两周（1209600秒），如果想修改这个时间，可以在项目的配置文件中添加如下所示的代码。</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 配置会话的超时时间为1天（86400秒）</span></span><br><span class="line">SESSION_COOKIE_AGE = <span class="number">86400</span></span><br></pre></td></tr></table></figure>

<p>有很多对安全性要求较高的应用都必须在关闭浏览器窗口时让会话过期，不再保留用户的任何信息，如果希望在关闭浏览器窗口时就让会话过期（cookie中的sessionid失效），可以加入如下所示的配置。</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 设置为True在关闭浏览器窗口时session就过期</span></span><br><span class="line">SESSION_EXPIRE_AT_BROWSER_CLOSE = <span class="literal">True</span></span><br></pre></td></tr></table></figure>

<p>如果不希望将session的数据保存在数据库中，可以将其放入缓存中，对应的配置如下所示，缓存的配置和使用我们在后面讲解。</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 配置将会话对象放到缓存中存储</span></span><br><span class="line">SESSION_ENGINE = <span class="string">&#x27;django.contrib.sessions.backends.cache&#x27;</span></span><br><span class="line"><span class="comment"># 配置使用哪一组缓存来保存会话</span></span><br><span class="line">SESSION_CACHE_ALIAS = <span class="string">&#x27;default&#x27;</span></span><br></pre></td></tr></table></figure>

<p>如果要修改session数据默认的序列化方式，可以将默认的<code>JSONSerializer</code>修改为<code>PickleSerializer</code>。</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SESSION_SERIALIZER = <span class="string">&#x27;django.contrib.sessions.serializers.PickleSerializer&#x27;</span></span><br></pre></td></tr></table></figure>

<p>接下来，我们就可以限制只有登录用户才能为老师投票，修改后的<code>praise_or_criticize</code>函数如下所示，我们通过从<code>request.session</code>中获取<code>userid</code>来判定用户是否登录。</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">praise_or_criticize</span>(<span class="params">request: HttpRequest</span>) -&gt; HttpResponse:</span></span><br><span class="line">    <span class="keyword">if</span> request.session.get(<span class="string">&#x27;userid&#x27;</span>):</span><br><span class="line">        <span class="keyword">try</span>:</span><br><span class="line">            tno = int(request.GET.get(<span class="string">&#x27;tno&#x27;</span>))</span><br><span class="line">            teacher = Teacher.objects.get(no=tno)</span><br><span class="line">            <span class="keyword">if</span> request.path.startswith(<span class="string">&#x27;/praise/&#x27;</span>):</span><br><span class="line">                teacher.good_count += <span class="number">1</span></span><br><span class="line">                count = teacher.good_count</span><br><span class="line">            <span class="keyword">else</span>:</span><br><span class="line">                teacher.bad_count += <span class="number">1</span></span><br><span class="line">                count = teacher.bad_count</span><br><span class="line">            teacher.save()</span><br><span class="line">            data = &#123;<span class="string">&#x27;code&#x27;</span>: <span class="number">20000</span>, <span class="string">&#x27;mesg&#x27;</span>: <span class="string">&#x27;投票成功&#x27;</span>, <span class="string">&#x27;count&#x27;</span>: count&#125;</span><br><span class="line">        <span class="keyword">except</span> (ValueError, Teacher.DoesNotExist):</span><br><span class="line">            data = &#123;<span class="string">&#x27;code&#x27;</span>: <span class="number">20001</span>, <span class="string">&#x27;mesg&#x27;</span>: <span class="string">&#x27;投票失败&#x27;</span>&#125;</span><br><span class="line">    <span class="keyword">else</span>:</span><br><span class="line">        data = &#123;<span class="string">&#x27;code&#x27;</span>: <span class="number">20002</span>, <span class="string">&#x27;mesg&#x27;</span>: <span class="string">&#x27;请先登录&#x27;</span>&#125;</span><br><span class="line">    <span class="keyword">return</span> JsonResponse(data)</span><br></pre></td></tr></table></figure>

<p>当然，在修改了视图函数后，<code>teachers.html</code>也需要进行调整，用户如果没有登录，就将用户引导至登录页，登录成功再返回到投票页，此处不再赘述。</p>
<h3 id="在视图函数中读写cookie"><a href="#在视图函数中读写cookie" class="headerlink" title="在视图函数中读写cookie"></a>在视图函数中读写cookie</h3><p>下面我们对如何使用cookie做一个更为细致的说明以便帮助大家在Web项目中更好的使用这项技术。Django封装的<code>HttpRequest</code>和<code>HttpResponse</code>对象分别提供了读写cookie的操作。</p>
<p>HttpRequest封装的属性和方法：</p>
<ol>
<li><code>COOKIES</code>属性 - 该属性包含了HTTP请求携带的所有cookie。</li>
<li><code>get_signed_cookie</code>方法 - 获取带签名的cookie，如果签名验证失败，会产生<code>BadSignature</code>异常。</li>
</ol>
<p>HttpResponse封装的方法：</p>
<ol>
<li><code>set_cookie</code>方法 - 该方法可以设置一组键值对并将其最终将写入浏览器。</li>
<li><code>set_signed_cookie</code>方法 - 跟上面的方法作用相似，但是会对cookie进行签名来达到防篡改的作用。因为如果篡改了cookie中的数据，在不知道<a target="_blank" rel="noopener" href="https://zh.wikipedia.org/wiki/%E5%AF%86%E9%92%A5">密钥</a>和<a target="_blank" rel="noopener" href="https://zh.wikipedia.org/wiki/%E7%9B%90_(%E5%AF%86%E7%A0%81%E5%AD%A6)">盐</a>的情况下是无法生成有效的签名，这样服务器在读取cookie时会发现数据与签名不一致从而产生<code>BadSignature</code>异常。需要说明的是，这里所说的密钥就是我们在Django项目配置文件中指定的<code>SECRET_KEY</code>，而盐是程序中设定的一个字符串，你愿意设定为什么都可以，只要是一个有效的字符串。</li>
</ol>
<p>上面提到的方法，如果不清楚它们的具体用法，可以自己查阅一下Django的<a target="_blank" rel="noopener" href="https://docs.djangoproject.com/en/2.1/ref/request-response/">官方文档</a>，没有什么资料比官方文档能够更清楚的告诉你这些方法到底如何使用。</p>
<p>刚才我们说过了，激活<code>SessionMiddleware</code>之后，每个<code>HttpRequest</code>对象都会绑定一个session属性，它是一个类似字典的对象，除了保存用户数据之外还提供了检测浏览器是否支持cookie的方法，包括：</p>
<ol>
<li><code>set_test_cookie</code>方法 - 设置用于测试的cookie。</li>
<li><code>test_cookie_worked</code>方法 - 检测测试cookie是否工作。</li>
<li><code>delete_test_cookie</code>方法 - 删除用于测试的cookie。</li>
<li><code>set_expiry</code>方法 - 设置会话的过期时间。</li>
<li><code>get_expire_age</code>/<code>get_expire_date</code>方法 - 获取会话的过期时间。</li>
<li><code>clear_expired</code>方法 - 清理过期的会话。</li>
</ol>
<p>下面是在执行登录之前检查浏览器是否支持cookie的代码。通常情况下，浏览器默认开启了对cookie的支持，但是可能因为某种原因，用户禁用了浏览器的cookie功能，遇到这种情况我们可以在视图函数中提供一个检查功能，如果检查到用户浏览器不支持cookie，可以给出相应的提示。</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">login</span>(<span class="params">request</span>):</span></span><br><span class="line">    <span class="keyword">if</span> request.method == <span class="string">&#x27;POST&#x27;</span>:</span><br><span class="line">        <span class="keyword">if</span> request.session.test_cookie_worked():</span><br><span class="line">            request.session.delete_test_cookie()</span><br><span class="line">            <span class="comment"># Add your code to perform login process here</span></span><br><span class="line">        <span class="keyword">else</span>:</span><br><span class="line">            <span class="keyword">return</span> HttpResponse(<span class="string">&quot;Please enable cookies and try again.&quot;</span>)</span><br><span class="line">    request.session.set_test_cookie()</span><br><span class="line">    <span class="keyword">return</span> render_to_response(<span class="string">&#x27;login.html&#x27;</span>)</span><br></pre></td></tr></table></figure>

<h3 id="Cookie的替代品"><a href="#Cookie的替代品" class="headerlink" title="Cookie的替代品"></a>Cookie的替代品</h3><p>之前我们说过了，cookie的名声一直都不怎么好，当然我们在实际开发中是不会在cookie中保存用户的敏感信息（如用户的密码、信用卡的账号等）的，而且保存在cookie中的数据一般也会做好编码和签名的工作。对于支持HTML5的浏览器来说，可以使用localStorage和sessionStorage做为cookie的替代方案，相信从名字上你就能听出二者的差别，存储在<code>localStorage</code>的数据可以长期保留；而存储在<code>sessionStorage</code>的数据会在浏览器关闭时会被清除 。关于这些cookie替代品的用法，建议大家查阅<a target="_blank" rel="noopener" href="https://developer.mozilla.org/zh-CN/docs/Web">MDN</a>来进行了解。 </p>
 
      <!-- reward -->
      
      <div id="reword-out">
        <div id="reward-btn">
          打赏
        </div>
      </div>
      
    </div>
    

    <!-- copyright -->
    
    <div class="declare">
      <ul class="post-copyright">
        <li>
          <i class="ri-copyright-line"></i>
          <strong>版权声明： </strong>
          
          本博客所有文章除特别声明外，著作权归作者所有。转载请注明出处！
          
        </li>
      </ul>
    </div>
    
    <footer class="article-footer">
       
<div class="share-btn">
      <span class="share-sns share-outer">
        <i class="ri-share-forward-line"></i>
        分享
      </span>
      <div class="share-wrap">
        <i class="arrow"></i>
        <div class="share-icons">
          
          <a class="weibo share-sns" href="javascript:;" data-type="weibo">
            <i class="ri-weibo-fill"></i>
          </a>
          <a class="weixin share-sns wxFab" href="javascript:;" data-type="weixin">
            <i class="ri-wechat-fill"></i>
          </a>
          <a class="qq share-sns" href="javascript:;" data-type="qq">
            <i class="ri-qq-fill"></i>
          </a>
          <a class="douban share-sns" href="javascript:;" data-type="douban">
            <i class="ri-douban-line"></i>
          </a>
          <!-- <a class="qzone share-sns" href="javascript:;" data-type="qzone">
            <i class="icon icon-qzone"></i>
          </a> -->
          
          <a class="facebook share-sns" href="javascript:;" data-type="facebook">
            <i class="ri-facebook-circle-fill"></i>
          </a>
          <a class="twitter share-sns" href="javascript:;" data-type="twitter">
            <i class="ri-twitter-fill"></i>
          </a>
          <a class="google share-sns" href="javascript:;" data-type="google">
            <i class="ri-google-fill"></i>
          </a>
        </div>
      </div>
</div>

<div class="wx-share-modal">
    <a class="modal-close" href="javascript:;"><i class="ri-close-circle-line"></i></a>
    <p>扫一扫，分享到微信</p>
    <div class="wx-qrcode">
      <img src="//api.qrserver.com/v1/create-qr-code/?size=150x150&data=http://example.com/2020/11/11/django/4-Cookie%E5%92%8CSession/" alt="微信分享二维码">
    </div>
</div>

<div id="share-mask"></div>  
  <ul class="article-tag-list" itemprop="keywords"><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/django/" rel="tag">django</a></li></ul>

    </footer>
  </div>

   
  <nav class="article-nav">
    
      <a href="/2020/11/11/django/2-%E6%B7%B1%E5%85%A5%E6%A8%A1%E5%9E%8B/" class="article-nav-link">
        <strong class="article-nav-caption">上一篇</strong>
        <div class="article-nav-title">
          
            2-深入模型.md
          
        </div>
      </a>
    
    
      <a href="/2020/11/11/django/9-RESTful%E6%9E%B6%E6%9E%84%E5%92%8CDRF%E5%85%A5%E9%97%A8/" class="article-nav-link">
        <strong class="article-nav-caption">下一篇</strong>
        <div class="article-nav-title">9-RESTful架构和DRF入门.md</div>
      </a>
    
  </nav>

   
<!-- valine评论 -->
<div id="vcomments-box">
  <div id="vcomments"></div>
</div>
<script src="//cdn1.lncld.net/static/js/3.0.4/av-min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/valine@1.4.14/dist/Valine.min.js"></script>
<script>
  new Valine({
    el: "#vcomments",
    app_id: "",
    app_key: "",
    path: window.location.pathname,
    avatar: "monsterid",
    placeholder: "给我的文章加点评论吧~",
    recordIP: true,
  });
  const infoEle = document.querySelector("#vcomments .info");
  if (infoEle && infoEle.childNodes && infoEle.childNodes.length > 0) {
    infoEle.childNodes.forEach(function (item) {
      item.parentNode.removeChild(item);
    });
  }
</script>
<style>
  #vcomments-box {
    padding: 5px 30px;
  }

  @media screen and (max-width: 800px) {
    #vcomments-box {
      padding: 5px 0px;
    }
  }

  #vcomments-box #vcomments {
    background-color: #fff;
  }

  .v .vlist .vcard .vh {
    padding-right: 20px;
  }

  .v .vlist .vcard {
    padding-left: 10px;
  }
</style>

 
     
</article>

</section>
      <footer class="footer">
  <div class="outer">
    <ul>
      <li>
        Copyrights &copy;
        2015-2020
        <i class="ri-heart-fill heart_icon"></i> TzWind
      </li>
    </ul>
    <ul>
      <li>
        
        
        
        由 <a href="https://hexo.io" target="_blank">Hexo</a> 强力驱动
        <span class="division">|</span>
        主题 - <a href="https://github.com/Shen-Yu/hexo-theme-ayer" target="_blank">Ayer</a>
        
      </li>
    </ul>
    <ul>
      <li>
        
        
        <span>
  <span><i class="ri-user-3-fill"></i>访问人数:<span id="busuanzi_value_site_uv"></span></s>
  <span class="division">|</span>
  <span><i class="ri-eye-fill"></i>浏览次数:<span id="busuanzi_value_page_pv"></span></span>
</span>
        
      </li>
    </ul>
    <ul>
      
    </ul>
    <ul>
      
    </ul>
    <ul>
      <li>
        <!-- cnzz统计 -->
        
        <script type="text/javascript" src='https://s9.cnzz.com/z_stat.php?id=1278069914&amp;web_id=1278069914'></script>
        
      </li>
    </ul>
  </div>
</footer>
      <div class="float_btns">
        <div class="totop" id="totop">
  <i class="ri-arrow-up-line"></i>
</div>

<div class="todark" id="todark">
  <i class="ri-moon-line"></i>
</div>

      </div>
    </main>
    <aside class="sidebar on">
      <button class="navbar-toggle"></button>
<nav class="navbar">
  
  <div class="logo">
    <a href="/"><img src="/images/ayer-side.svg" alt="Hexo"></a>
  </div>
  
  <ul class="nav nav-main">
    
    <li class="nav-item">
      <a class="nav-item-link" href="/">主页</a>
    </li>
    
    <li class="nav-item">
      <a class="nav-item-link" href="/archives">归档</a>
    </li>
    
    <li class="nav-item">
      <a class="nav-item-link" href="/categories">分类</a>
    </li>
    
    <li class="nav-item">
      <a class="nav-item-link" href="/tags">标签</a>
    </li>
    
    <li class="nav-item">
      <a class="nav-item-link" target="_blank" rel="noopener" href="http://www.baidu.com">百度</a>
    </li>
    
    <li class="nav-item">
      <a class="nav-item-link" href="/friends">友链</a>
    </li>
    
    <li class="nav-item">
      <a class="nav-item-link" href="/2019/about">关于我</a>
    </li>
    
  </ul>
</nav>
<nav class="navbar navbar-bottom">
  <ul class="nav">
    <li class="nav-item">
      
      <a class="nav-item-link nav-item-search"  title="搜索">
        <i class="ri-search-line"></i>
      </a>
      
      
      <a class="nav-item-link" target="_blank" href="/atom.xml" title="RSS Feed">
        <i class="ri-rss-line"></i>
      </a>
      
    </li>
  </ul>
</nav>
<div class="search-form-wrap">
  <div class="local-search local-search-plugin">
  <input type="search" id="local-search-input" class="local-search-input" placeholder="Search...">
  <div id="local-search-result" class="local-search-result"></div>
</div>
</div>
    </aside>
    <script>
      if (window.matchMedia("(max-width: 768px)").matches) {
        document.querySelector('.content').classList.remove('on');
        document.querySelector('.sidebar').classList.remove('on');
      }
    </script>
    <div id="mask"></div>

<!-- #reward -->
<div id="reward">
  <span class="close"><i class="ri-close-line"></i></span>
  <p class="reward-p"><i class="ri-cup-line"></i>请我喝杯咖啡吧~</p>
  <div class="reward-box">
    
    
  </div>
</div>
    
<script src="/js/jquery-2.0.3.min.js"></script>


<script src="/js/lazyload.min.js"></script>

<!-- Tocbot -->


<script src="/js/tocbot.min.js"></script>

<script>
  tocbot.init({
    tocSelector: '.tocbot',
    contentSelector: '.article-entry',
    headingSelector: 'h1, h2, h3, h4, h5, h6',
    hasInnerContainers: true,
    scrollSmooth: true,
    scrollContainer: 'main',
    positionFixedSelector: '.tocbot',
    positionFixedClass: 'is-position-fixed',
    fixedSidebarOffset: 'auto'
  });
</script>

<script src="https://cdn.jsdelivr.net/npm/jquery-modal@0.9.2/jquery.modal.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/jquery-modal@0.9.2/jquery.modal.min.css">
<script src="https://cdn.jsdelivr.net/npm/justifiedGallery@3.7.0/dist/js/jquery.justifiedGallery.min.js"></script>

<script src="/dist/main.js"></script>

<!-- ImageViewer -->

<!-- Root element of PhotoSwipe. Must have class pswp. -->
<div class="pswp" tabindex="-1" role="dialog" aria-hidden="true">

    <!-- Background of PhotoSwipe. 
         It's a separate element as animating opacity is faster than rgba(). -->
    <div class="pswp__bg"></div>

    <!-- Slides wrapper with overflow:hidden. -->
    <div class="pswp__scroll-wrap">

        <!-- Container that holds slides. 
            PhotoSwipe keeps only 3 of them in the DOM to save memory.
            Don't modify these 3 pswp__item elements, data is added later on. -->
        <div class="pswp__container">
            <div class="pswp__item"></div>
            <div class="pswp__item"></div>
            <div class="pswp__item"></div>
        </div>

        <!-- Default (PhotoSwipeUI_Default) interface on top of sliding area. Can be changed. -->
        <div class="pswp__ui pswp__ui--hidden">

            <div class="pswp__top-bar">

                <!--  Controls are self-explanatory. Order can be changed. -->

                <div class="pswp__counter"></div>

                <button class="pswp__button pswp__button--close" title="Close (Esc)"></button>

                <button class="pswp__button pswp__button--share" style="display:none" title="Share"></button>

                <button class="pswp__button pswp__button--fs" title="Toggle fullscreen"></button>

                <button class="pswp__button pswp__button--zoom" title="Zoom in/out"></button>

                <!-- Preloader demo http://codepen.io/dimsemenov/pen/yyBWoR -->
                <!-- element will get class pswp__preloader--active when preloader is running -->
                <div class="pswp__preloader">
                    <div class="pswp__preloader__icn">
                        <div class="pswp__preloader__cut">
                            <div class="pswp__preloader__donut"></div>
                        </div>
                    </div>
                </div>
            </div>

            <div class="pswp__share-modal pswp__share-modal--hidden pswp__single-tap">
                <div class="pswp__share-tooltip"></div>
            </div>

            <button class="pswp__button pswp__button--arrow--left" title="Previous (arrow left)">
            </button>

            <button class="pswp__button pswp__button--arrow--right" title="Next (arrow right)">
            </button>

            <div class="pswp__caption">
                <div class="pswp__caption__center"></div>
            </div>

        </div>

    </div>

</div>

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/photoswipe@4.1.3/dist/photoswipe.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/photoswipe@4.1.3/dist/default-skin/default-skin.min.css">
<script src="https://cdn.jsdelivr.net/npm/photoswipe@4.1.3/dist/photoswipe.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/photoswipe@4.1.3/dist/photoswipe-ui-default.min.js"></script>

<script>
    function viewer_init() {
        let pswpElement = document.querySelectorAll('.pswp')[0];
        let $imgArr = document.querySelectorAll(('.article-entry img:not(.reward-img)'))

        $imgArr.forEach(($em, i) => {
            $em.onclick = () => {
                // slider展开状态
                // todo: 这样不好，后面改成状态
                if (document.querySelector('.left-col.show')) return
                let items = []
                $imgArr.forEach(($em2, i2) => {
                    let img = $em2.getAttribute('data-idx', i2)
                    let src = $em2.getAttribute('data-target') || $em2.getAttribute('src')
                    let title = $em2.getAttribute('alt')
                    // 获得原图尺寸
                    const image = new Image()
                    image.src = src
                    items.push({
                        src: src,
                        w: image.width || $em2.width,
                        h: image.height || $em2.height,
                        title: title
                    })
                })
                var gallery = new PhotoSwipe(pswpElement, PhotoSwipeUI_Default, items, {
                    index: parseInt(i)
                });
                gallery.init()
            }
        })
    }
    viewer_init()
</script>

<!-- MathJax -->

<!-- Katex -->

<!-- busuanzi  -->


<script src="/js/busuanzi-2.3.pure.min.js"></script>


<!-- ClickLove -->

<!-- ClickBoom1 -->

<!-- ClickBoom2 -->

<!-- CodeCopy -->


<link rel="stylesheet" href="/css/clipboard.css">

<script src="https://cdn.jsdelivr.net/npm/clipboard@2/dist/clipboard.min.js"></script>
<script>
  function wait(callback, seconds) {
    var timelag = null;
    timelag = window.setTimeout(callback, seconds);
  }
  !function (e, t, a) {
    var initCopyCode = function(){
      var copyHtml = '';
      copyHtml += '<button class="btn-copy" data-clipboard-snippet="">';
      copyHtml += '<i class="ri-file-copy-2-line"></i><span>COPY</span>';
      copyHtml += '</button>';
      $(".highlight .code pre").before(copyHtml);
      $(".article pre code").before(copyHtml);
      var clipboard = new ClipboardJS('.btn-copy', {
        target: function(trigger) {
          return trigger.nextElementSibling;
        }
      });
      clipboard.on('success', function(e) {
        let $btn = $(e.trigger);
        $btn.addClass('copied');
        let $icon = $($btn.find('i'));
        $icon.removeClass('ri-file-copy-2-line');
        $icon.addClass('ri-checkbox-circle-line');
        let $span = $($btn.find('span'));
        $span[0].innerText = 'COPIED';
        
        wait(function () { // 等待两秒钟后恢复
          $icon.removeClass('ri-checkbox-circle-line');
          $icon.addClass('ri-file-copy-2-line');
          $span[0].innerText = 'COPY';
        }, 2000);
      });
      clipboard.on('error', function(e) {
        e.clearSelection();
        let $btn = $(e.trigger);
        $btn.addClass('copy-failed');
        let $icon = $($btn.find('i'));
        $icon.removeClass('ri-file-copy-2-line');
        $icon.addClass('ri-time-line');
        let $span = $($btn.find('span'));
        $span[0].innerText = 'COPY FAILED';
        
        wait(function () { // 等待两秒钟后恢复
          $icon.removeClass('ri-time-line');
          $icon.addClass('ri-file-copy-2-line');
          $span[0].innerText = 'COPY';
        }, 2000);
      });
    }
    initCopyCode();
  }(window, document);
</script>


<!-- CanvasBackground -->


    
  </div>
</body>

</html>